Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggested implementation of themes refactoring #1340

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

manuhabitela
Copy link
Collaborator

@manuhabitela manuhabitela commented Dec 12, 2024

Context

Here is an example of implementation following discussions in #1295 (up until George's comment).

Proposed solution

Everything is detailed in comments and commit messages, here is a summary of it:

  • 1st commit: add CSS layers to make sure default variables and themes variables do not collapse (this is helpful because the idea of this implementation is a theme can now directly override default variables)
  • 2nd commit: instead of having cssVars.colors and cssVars.vars, have one big unique object that holds all the design tokens (cssVars.designTokens), that can all be overriden by a theme. Contrary to before where default colors could not be changed. The idea would be that the new object replaces the 2 others.
  • 3rd commit: in cssVars.theme, make the specific component variables target the new design tokens. In the end, in each Theme, specific component variables can be removed, and we can decide to define only the more global design token values (see GristDark).
    • In a different branch, I also tried another usage as an alternative to 3rd commit: removing component-specific variables. This leads to more code changes across files but also to a smaller global theme-related code in the end.

Questions that popped up:

  • on this implementation, now a theme has the possibility to define only a few variables, with the default values of cssVars being used for the undefined variables. I'm not sure this is a great idea as it could lead to oversights when implementing new themes or updating current ones. I'm not sure it's actually an issue either. As we can easily create a new Theme by extending an existing one anyway. What do you think?

Related issues

#1295

Has this been tested?

  • 👍 yes, I added tests to the test suite
  • 💭 no, because this PR is a draft and still needs work
  • 🙅 no, because this is not relevant here
  • 🙋 no, because I need help

ping @dsagal @georgegevoian

@manuhabitela manuhabitela marked this pull request as draft December 12, 2024 11:01
@manuhabitela manuhabitela force-pushed the issue-1295-theme-refactoring branch from 9d33bcc to e65a3f0 Compare December 12, 2024 11:30
- introduce "grist-base", "grist-theme" and "grist-custom" css layers in
order to easily handle priority of css variables and base css rules

- apply css default variables and css reset on the "grist-base" layer.
Note that the way default html/body css is loaded has been changed to be
simpler, but maybe I assumed too much here, I didn't dig too deep to get
the whole context

- apply custom theme css variables on the "grist-theme" layer

- a potential custom.css file should wrap its code in the
"grist-custom" layer, as the updated example file suggests

This will allow base/theme/custom CSS to generate the same variable
names while being sure custom css takes precedence theme variables, and
theme variables take precedence over default variables.
Until now themes could only describe each component variable. This made
creating a theme difficult: we had to override all component variables
(hundreds of them) if we wanted to change one color shade used globally.

Here is an idea of a cssVars.designTokens prop that is a combination of
current `colors`, `vars` and `theme` props:

- all design tokens are overridable by a theme
- colors are now named in a more abstract way so that it makes sense for
a theme changing the primary color

Everything is explained in details in the cssVars.designTokens comments
Use new design tokens on right panel header tabs, without removing
component-specific variables.

Component-specific variables now target designTokens, that are
themselves overridable by a theme, allowing to prevent repeating lots of
definitions in the theme file.
@manuhabitela manuhabitela force-pushed the issue-1295-theme-refactoring branch from e65a3f0 to f9027ce Compare December 12, 2024 11:39
@georgegevoian
Copy link
Contributor

Approach makes sense to me. Thank you.

on this implementation, now a theme has the possibility to define only a few variables, with the default values of cssVars being used for the undefined variables. I'm not sure this is a great idea as it could lead to oversights when implementing new themes or updating current ones. I'm not sure it's actually an issue either. As we can easily create a new Theme by extending an existing one anyway. What do you think?

Making all the tokens required in ThemeColors would be my preference. In fact, I'd be interested in taking the refactor further and trying to get it down to 3 layers: grist-base, grist-theme, and grist-custom. My thinking is a theme should always be applied, so we could try always applying the light theme on initial setup (i.e. from attachCssRootVars), which would replace the role that grist-tokens currently serves. Do you see any problems with doing that?

@manuhabitela
Copy link
Collaborator Author

Approach makes sense to me. Thank you.

Great :)

Making all the tokens required in ThemeColors would be my preference

So to be 100% clear, you mean at least this?

  • a theme object (like GristLight.ts), should define at its root, all the colors, the paddings, font sizes, z-indexes, etc, as now the "design tokens" are a mix between colors and vars,

But do you also mean this?

  • it should define in its legacyVariables prop (current naming in the PR), the 400+ component-specific variables, with stuff mostly targeting the design tokens.

I'd argue if we think those component-specific variables are kinda legacy and we eventually want to remove them, I'd say making them required in a theme object is maybe too much trouble? Since the idea is themes would not want to change those, as cssVars.theme would mostly default to design tokens.

Here are a few thoughts:

  • if a theme *has* to implement all designTokens, there would be no need to define any value in cssVars.designTokens, since all actual values would always come from the current theme. The definition of those objects would then mostly help only to easily target css variables from the JS code when writing css. Maybe I could update the code to more reflect that and stop defining actual values in designTokens, but only define css variable keys?
  • would it make sense to change how we set up values in a theme? Instead of having props matching css variable names, having them match the designTokens and theme keys. Maybe feels weird to have all the code target the "js props" while a theme targets "css variables"?
  • I'd say having some 'BaseTheme' object that lists all the default stuff would be helpful. Like all the padding/fonts/zindex. So that a theme could be easily defined by listing all its colors, that wouldn't be in the BaseTheme, and merging the BaseTheme, for all the stuff that is common.

In fact, I'd be interested in taking the refactor further and trying to get it down to 3 layers: grist-base, grist-theme, and grist-custom. My thinking is a theme should always be applied, so we could try always applying the light theme on initial setup (i.e. from attachCssRootVars), which would replace the role that grist-tokens currently serves.

Yeah I guess this way of doing was more to show the structure, indeed "grist-tokens" is not necessary. I'd argue if *everything* is listed in a theme object, the concept of "grist-base" variables is also debatable: every design token variable is tied to the theme, and then there is the custom css layer that comes on top.

@georgegevoian
Copy link
Contributor

So to be 100% clear, you mean at least this?

a theme object (like GristLight.ts), should define at its root, all the colors, the paddings, font sizes, z-indexes, etc, as now > the "design tokens" are a mix between colors and vars,

Only the variables we'd like to change from theme to theme. So yes to colors, but no to z-indexes.

Padding and font size are more interesting; I can see an argument that making them required would be more of a nuisance, as they won't always change across themes. A compromise might be to make the vars (e.g. fonts, padding, etc.) optional, and the colors required, with the vars being inherited from the base layer.

But do you also mean this?

it should define in its legacyVariables prop (current naming in the PR), the 400+ component-specific variables, with stuff mostly targeting the design tokens.
I'd argue if we think those component-specific variables are kinda legacy and we eventually want to remove them, I'd say making them required in a theme object is maybe too much trouble? Since the idea is themes would not want to change > those, as cssVars.theme would mostly default to design tokens.

No, I agree that it's too much trouble to make those required.

Here are a few thoughts:

  • if a theme has to implement all designTokens, there would be no need to define any value in cssVars.designTokens, since all actual values would always come from the current theme. The definition of those objects would then mostly help only to easily target css variables from the JS code when writing css. Maybe I could update the code to more reflect that and stop defining actual values in designTokens, but only define css variable keys?
  • would it make sense to change how we set up values in a theme? Instead of having props matching css variable names, having them match the designTokens and theme keys. Maybe feels weird to have all the code target the "js props" while a theme targets "css variables"?
  • I'd say having some 'BaseTheme' object that lists all the default stuff would be helpful. Like all the padding/fonts/zindex. So that a theme could be easily defined by listing all its colors, that wouldn't be in the BaseTheme, and merging the BaseTheme, for all the stuff that is common.

I had a similar thing in mind. A base theme defining the default, non-color variables makes a lot of sense to me. The current system is awkward because things live in two places, as you said. If we can narrow that down to one, that'd be great!

- there is no more default values set in cssVars for vars being tied to
a theme, meaning `colors` and `vars` do not have any value anymore. They
just expose the new `tokens` object
- a new theme `tokens` object exposes all the CSS variables related to
the current theme. They include colors, but also font family, paddings,
basically everything that was described in cssVars colors and vars.
- a new theme `legacyVariables` object exposes all the previous cssVars
`theme` variables that are considered legacy code. The cssVars `theme`
object just consumes this new object
- themes now must define tokens that were before only in cssVars
`colors` and ` vars`. To help with that, there is now a Base.ts theme
that describes all variables that should stay the same for most themes.
- in practice, defining a new theme will mean meandescribing an object a
couple dozens variables, instead of the 400+ previously.
- Removed the use of ts-interface-builder as it didn't work with new
types (made with generics). the gristUrls code was updated accordingly.
But the AppModel that used the ThemePrefsChecker now misses the check. I
actually don't understand why it was actually necessary here. Removed it
for now, I need some feedback.

- example of "legacy variables" handling has begun: some more generic
token variables were added, that a set of legacy variables target
instead of previous specific values. it shows how we could try to
refactor the bunch of variables so that updating themes would then be
easier. The added variables are ones matching variables that were used a
lot in grist dark and grist light themes.
@manuhabitela
Copy link
Collaborator Author

manuhabitela commented Jan 20, 2025

Hey @georgegevoian,

Here is an update to move things forward.

Summary

The idea is:

  • Actual values are now only in themes, except for css vars that are really theme-agnostic, which means now only the z-index list. All other variables (colors, font families, paddings, font sizes, etc) can now be modified in a theme.

  • New tokens and legacyVariables objects list CustomProp instances for all theme tokens. These are here mostly so that the codebase can consume the theme tokens.
    For example, instead of writing this in a component code:

    import {colors, vars, theme} from 'app/client/ui2018/cssVars';
    export const button = styled('button', `
      font-size: ${vars.mediumFontSize};
      color: ${colors.slate};
      background-color: ${theme.controlPrimaryBg};
    `);

    We would now write:

    import {tokens, legacyVariables} from 'app/common/ThemePrefs';
    export const button = styled('button', `
      font-size: ${tokens.mediumFontSize};
      color: ${tokens.slate};
      background-color: ${legacyVariables.controlPrimaryBg};
    `);

    For now, to prevent changing hundreds of files and have an even more difficult diff to read (and also while this is still an ongoing discussion :)), old cssVars colors, vars and theme are still there, but just target the new objects.

  • A new Base theme implements the theme tokens that we assume won't change much between actual themes. It mostly lists old vars and theme variables, the grayscale that was in colors, and added generic variables. The theme variables are listed under a legacyVariables property as suggested before.

  • Each theme must describe mandatory keys, that are not in the Base theme: specific colors.

  • I started some work to add some generic variables to let us implement less things on a specific theme's legacyVariables. You can see around 230 legacy variables now target generic tokens, while 200 others are still implemented in each theme. This gist helps in finding common values mapping between the light and dark themes.

Feedback needed

Variable namings and impact on widget's styling

I made the choice to abstract the css variable names as much as possible. The goal is that we don't have to regularly switch in our heads between the consumed in the codebase name (tokens.textLight), vs the described in the theme name (theme-color-text-light).

So there is some code to map some js object keys to existing css var names.

Unfortunately I only figured out later that theme variables in widget iframes was loaded differently. And from what I understand I can't import common files in the plugin-related code? Which means for now I can't go find the css variable name matching the theme key in the plugin code, like I do in the default attachCssThemeVars.

Any advice on what I could do there?

My intuition would be to actually remove the js object key <> css var name manual mapping and just generate css variable names based on the JS keys. So that tokens.textLight would generate a css variable --grist-textLight automatically. I did a manual mapping and kept the current css names for potential backward compatibility with custom css files and to prevent having to update files here and there directly consuming css variables… But your input would be helpful, from my limited point of view I'd find it simpler if we didn't have to handle this mapping. I actually don't get what is the value in handling ourselves the css variable names instead of relying on our JS namings that we use across most of the app.

TS checkers

I had trouble with the ts-interface-checker related code, because it doesn't handle generics. I ended up just… removing the ThemePrefs-ti file. So I replaced a specific part of the app where some checkers were used (in gristUrls) to make the check manually. And I removed the checker in AppModel. I actually don't understand that much why this checker is needed here, since the themes are checked by typescript at build time and from my understand at this stage the theme can't come from a random runtime value?

I'm not against a little bit of explanation here if what I did is not ok, I assume I missed something :)

Next steps

If what you see seems globally find for you, here what I wanted to continue on:

  • fix the theme variables loading for iframes
  • actually remove cssVars.ts colors, vars and theme and make the whole codebase target the new tokens variable. I guess changing this in one big commit would be better than leaving 2 ways of consuming design tokens in the codebase
  • think a little more about new generic names for the legacyVariables, meaning, continuing the work I started with the 230 vs 200 legacyVariables (see above). This includes both putting out of "legacyVariables" some interesting variables (like input or control related ones), and coming up with new generic variables that legacy variables would then consume. My goal is not to clean this very thoroughly as there are a lot of stuff. But at least cleaning the variables used in multiple places and come up with better namings. I don't see a lot of value in taking hours cleaning all the variables used in only specific places, but I see some in cleaning the variables used more globally.

What do you think?

Thanks a lot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants